Task 1¶

The following are tables of the Blue and Red groups and the model decisions/probabilities. We will use them to calculate the fairness coefficients.

Blue Will use XAI (1) Will not use XAI (0) Total
Enrolled in training (1) 60 5 65
Not enrolled in training (0) 20 15 35
Total 80 20 100
Red Will use XAI (1) Will not use XAI (0) Total
Enrolled in training (1) 25% 25% 50%
Not enrolled in training (0) 25% 25% 50%
Total 50% 50% 100%

Demographic parity¶

Blue: $ P(\hat{Y}=1 | color=Blue) = 65 / (65+35) = 0.65$

Red: $ P(\hat{Y}=1 | color=Red) = 50\% / (50\%+50\%) = 0.5$

Red is slightly underprivileged. The Coefficient here is $\frac{0.65}{0.5} = 130\% > 125\%$

Equal opportunity¶

Blue: $ P(\hat{Y}=1 | Y = 1, color=Blue) = 60 / (60+20) = 0.75$

Red: $ P(\hat{Y}=1 | Y = 1, color=Red) = 25\% / (25\%+25\%) = 0.5$

Red is underprivileged. The Coefficient here is $\frac{0.75}{0.5} = 150\% > 125\%$

Predictive rate parity¶

Blue: $ P(Y = 1 | \hat{Y}=1, color=Blue) = 60 / (60+5) \approx 0.92$

Red: $ P(Y = 1| \hat{Y}=1, color=Red) = 25\% / (25\%+25\%) = 0.5$

Red is very underprivileged. The Coefficient here is $\frac{0.92}{0.5} \approx 184\% > 125\%$

Starred task - improving fairness¶

Assuming it is very hard to create a predictive model for Red and we assign the enrollments there randomly, we can still change the percentage of the group getting the enrollment. Noting that in the case of random enrollment, since then $\hat{Y} \perp Y$, by changing the rate of assingment in the red group, we directly change the demographic parity coeeficient and the equal opportunity coefficient. As such, increasing the rate of assignment to 65% instantly changes:

Demographic parity: $P(\hat{Y}=1 | color=Red) = 65\% / (65\%+35\%) = 0.65$

Equal opportunity: $P(\hat{Y}=1 | Y = 1, color=Red) = P(\hat{Y}=1 | color=Red) = 65\% / (65\%+35\%) = 0.65$

In both cases the coefficient improves: Demographic parity: $\frac{0.65}{0.65} = 100\% \leq 125\%$

Equal opportunity: $\frac{0.75}{0.65} \approx 115\% \leq 125\%$

Since $\hat{Y} \perp Y$, the Predictive rate parity does not change at all and remains at the same levels.

This way, we improved on 2 of the 3 equality coefficients, with the third remaining constant.

Task 2¶

Dataset selection and model training¶

We decide to use the Adult income dataset (https://www.kaggle.com/datasets/wenruliu/adult-income-dataset/). The purpose of the dataset is to train a model in predicting whether a person has over 50K income. After quickly cleaning the data (conversion of categorical columns into one-hot encoded), we divided the data randomly into 90% train and 10% test subsets. We proceeded to train predictive models and check their accuracy as well as their bias against females.

Models we trained were:

  1. Logistic regression
  2. Random Forest
  3. Logistic regression with bonus points for being female

Results¶

Train accuracy Test accuracy TPR (female) ACC (female) PPV (female) FPR (female) STP (female)
Logistic regression 0.796 0.801 0.992032 1.188579 0.663087 0.833333 0.53
Random Forest 0.863 0.858 0.729433 1.125 1.012626 0.183333 0.254902
Logistic regression (bonus 0.1)* 0.796 0.800 1.035857 1.184595 0.62953 0.944444 0.58
Logistic regression (bonus 0.2)* 0.795 0.798 1.059761 1.177955 0.58255 1.138889 0.64
Logistic regression (bonus 0.3)* 0.795 0.797 1.059761 1.176627 0.571812 1.166667 0.65
Logistic regression (bonus 0.4)* 0.793 0.796 1.059761 1.169987 0.536913 1.305556 0.7
Logistic regression (bonus 0.5)* 0.790 0.792 1.083665 1.155378 0.467114 1.638889 0.82

Note: *Bonus points assigned to female when predicting high income; Bolded highest accuracies; Bolded fairness ratios within the 4/5 rule

chart

Comments¶

  1. All models trained have only at most 3 fairness metrics satisfied simultaneously
  2. Random forest has the best accuracy, but the worst TPR, FPR and STP
  3. Logistic regression and Random forest models behave completely different in terms of the fairness coefficients
  4. Increasing the bonus in logistic regression decreases its accuracy
  5. Increasing the bonus in logistic regression increases the TPR, FPR and STP, while decreasing ACC and PPV

Appendix¶

Data preparation¶

In [3]:
import copy

import numpy as np
import pandas as pd

df = pd.read_csv('adult.csv')
df.head()
Out[3]:
age workclass fnlwgt education educational-num marital-status occupation relationship race gender capital-gain capital-loss hours-per-week native-country income
0 25 Private 226802 11th 7 Never-married Machine-op-inspct Own-child Black Male 0 0 40 United-States <=50K
1 38 Private 89814 HS-grad 9 Married-civ-spouse Farming-fishing Husband White Male 0 0 50 United-States <=50K
2 28 Local-gov 336951 Assoc-acdm 12 Married-civ-spouse Protective-serv Husband White Male 0 0 40 United-States >50K
3 44 Private 160323 Some-college 10 Married-civ-spouse Machine-op-inspct Husband Black Male 7688 0 40 United-States >50K
4 18 ? 103497 Some-college 10 Never-married ? Own-child White Female 0 0 30 United-States <=50K
In [ ]:
for col in df:
    print(f"Column {col}: {df[col].unique()}")
In [5]:
df.dtypes
Out[5]:
age                 int64
workclass          object
fnlwgt              int64
education          object
educational-num     int64
marital-status     object
occupation         object
relationship       object
race               object
gender             object
capital-gain        int64
capital-loss        int64
hours-per-week      int64
native-country     object
income             object
dtype: object
In [ ]:
df2 = pd.get_dummies(df)
df2.head()
In [ ]:
del df2[df2.columns[-2]]
df2.head()
In [8]:
X = df2.loc[:, df2.columns != 'income_>50K']
y = df2.loc[:, df2.columns == 'income_>50K']

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=2)

Model 1 - Logistic Regression¶

In [9]:
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
regr = LogisticRegression(random_state=2).fit(X_train, y_train)
print(f"Accuracy: Train: {accuracy_score(y_train, regr.predict(X_train))} Test: {accuracy_score(y_test, regr.predict(X_test))}")
C:\Users\Antek\anaconda3\lib\site-packages\sklearn\utils\validation.py:1111: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
  y = column_or_1d(y, warn=True)
Accuracy: Train: 0.7975066542302705 Test: 0.8010235414534289
In [10]:
import dalex as dx
In [166]:
explainer = dx.Explainer(regr, X_test, y_test, label='Logistic Regression', verbose=False)
explainer.model_performance()
C:\Users\Antek\anaconda3\lib\site-packages\sklearn\base.py:450: UserWarning:

X does not have valid feature names, but LogisticRegression was fitted with feature names

Out[166]:
recall precision f1 accuracy auc
Logistic Regression 0.250664 0.691932 0.36801 0.801024 0.577077
In [13]:
protected_variable = X_test.gender_Male.apply(lambda x: "male" if x else "female")
privileged_group = "male"

fobject = explainer.model_fairness(
    protected=protected_variable,
    privileged=privileged_group
)
fobject.fairness_check()
Bias detected in 2 metrics: PPV, STP

Conclusion: your model is not fair because 2 or more criteria exceeded acceptable limits set by epsilon.

Ratios of metrics, based on 'male'. Parameter 'epsilon' was set to 0.8 and therefore metrics should be within (0.8, 1.25)
             TPR       ACC       PPV       FPR   STP
female  0.992032  1.188579  0.663087  0.833333  0.53
In [14]:
fobject.plot()

Model 2 - Random Forest¶

In [160]:
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(random_state=2, max_depth=10).fit(X_train, y_train)
print(f"Accuracy: Train: {accuracy_score(y_train, rf.predict(X_train))} Test: {accuracy_score(y_test, rf.predict(X_test))}")
C:\Users\Antek\AppData\Local\Temp\ipykernel_11860\4048729203.py:2: DataConversionWarning:

A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples,), for example using ravel().

Accuracy: Train: 0.8636167163364197 Test: 0.858546571136131
In [167]:
explainer2 = dx.Explainer(rf, X_test, y_test, label='Random Forest', verbose=False)
explainer2.model_performance()
C:\Users\Antek\anaconda3\lib\site-packages\sklearn\base.py:450: UserWarning:

X does not have valid feature names, but RandomForestClassifier was fitted with feature names

Out[167]:
recall precision f1 accuracy auc
Random Forest 0.524358 0.793566 0.631467 0.858547 0.908062
In [162]:
protected_variable = X_test.gender_Male.apply(lambda x: "male" if x else "female")
privileged_group = "male"

fobject2 = explainer2.model_fairness(
    protected=protected_variable,
    privileged=privileged_group
)
fobject2.fairness_check()
Bias detected in 3 metrics: TPR, FPR, STP

Conclusion: your model is not fair because 2 or more criteria exceeded acceptable limits set by epsilon.

Ratios of metrics, based on 'male'. Parameter 'epsilon' was set to 0.8 and therefore metrics should be within (0.8, 1.25)
             TPR    ACC       PPV       FPR       STP
female  0.729433  1.125  1.012626  0.183333  0.254902
In [163]:
fobject2.plot()

Model 3 - Logistic Regression with bias mitigation¶

In [133]:
class regr_with_bonus(LogisticRegression):
    def __init__(self, attribute_to_bonus='gender_Female', bonus_amount=0, **kwargs):
        LogisticRegression.__init__(self, **kwargs)
        self.attribute_to_bonus = attribute_to_bonus
        self.bonus_amount = bonus_amount
    def predict(self, X):
        a = (((np.dot(X, self.coef_.T)+self.intercept_+(self.bonus_amount*X[[self.attribute_to_bonus]]))>0)*1).to_numpy()
        return a.ravel()
In [144]:
regr3 = regr_with_bonus(random_state=2, attribute_to_bonus='gender_Female', bonus_amount=0.1).fit(X_train, y_train)
C:\Users\Antek\anaconda3\lib\site-packages\sklearn\utils\validation.py:1111: DataConversionWarning:

A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().

In [145]:
print(f"Accuracy: Train: {accuracy_score(y_train, regr3.predict(X_train))} Test: {accuracy_score(y_test, regr3.predict(X_test))}")
Accuracy: Train: 0.7965284255067452 Test: 0.8
In [137]:
def predictFunction(model, data):
    return model.predict(data)
In [175]:
from copy import deepcopy
bonus_models = []
for female_bonus in [0.1, 0.2, 0.3, 0.4, 0.5]:
    print("Bonus: ",female_bonus)
    regr3 = regr_with_bonus(random_state=2, attribute_to_bonus='gender_Female', bonus_amount=female_bonus).fit(X_train, y_train)
    print(f"Accuracy: Train: {accuracy_score(y_train, regr3.predict(X_train))} Test: {accuracy_score(y_test, regr3.predict(X_test))}")
    explainer4 = dx.Explainer(regr3, X_test, y_test, predict_function=predictFunction, verbose=False, label='LogisticRegression with female bonus '+str(female_bonus))
    fobject4 = explainer4.model_fairness(
    protected=protected_variable,
    privileged=privileged_group)
    fobject4.fairness_check()
    bonus_models.append(deepcopy(fobject4))
Bonus:  0.1
C:\Users\Antek\anaconda3\lib\site-packages\sklearn\utils\validation.py:1111: DataConversionWarning:

A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().

Accuracy: Train: 0.7965284255067452 Test: 0.8
Bias detected in 2 metrics: PPV, STP

Conclusion: your model is not fair because 2 or more criteria exceeded acceptable limits set by epsilon.

Ratios of metrics, based on 'male'. Parameter 'epsilon' was set to 0.8 and therefore metrics should be within (0.8, 1.25)
             TPR       ACC      PPV       FPR   STP
female  1.035857  1.184595  0.62953  0.944444  0.58
Bonus:  0.2
C:\Users\Antek\anaconda3\lib\site-packages\sklearn\utils\validation.py:1111: DataConversionWarning:

A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().

Accuracy: Train: 0.795732192824806 Test: 0.7983623336745138
Bias detected in 2 metrics: PPV, STP

Conclusion: your model is not fair because 2 or more criteria exceeded acceptable limits set by epsilon.

Ratios of metrics, based on 'male'. Parameter 'epsilon' was set to 0.8 and therefore metrics should be within (0.8, 1.25)
             TPR       ACC      PPV       FPR   STP
female  1.059761  1.177955  0.58255  1.138889  0.64
Bonus:  0.3
C:\Users\Antek\anaconda3\lib\site-packages\sklearn\utils\validation.py:1111: DataConversionWarning:

A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().

Accuracy: Train: 0.7951634551948495 Test: 0.7979529170931423
Bias detected in 2 metrics: PPV, STP

Conclusion: your model is not fair because 2 or more criteria exceeded acceptable limits set by epsilon.

Ratios of metrics, based on 'male'. Parameter 'epsilon' was set to 0.8 and therefore metrics should be within (0.8, 1.25)
             TPR       ACC       PPV       FPR   STP
female  1.059761  1.176627  0.571812  1.166667  0.65
Bonus:  0.4
C:\Users\Antek\anaconda3\lib\site-packages\sklearn\utils\validation.py:1111: DataConversionWarning:

A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().

Accuracy: Train: 0.7936619878517642 Test: 0.7965199590583418
Bias detected in 3 metrics: PPV, FPR, STP

Conclusion: your model is not fair because 2 or more criteria exceeded acceptable limits set by epsilon.

Ratios of metrics, based on 'male'. Parameter 'epsilon' was set to 0.8 and therefore metrics should be within (0.8, 1.25)
             TPR       ACC       PPV       FPR  STP
female  1.059761  1.169987  0.536913  1.305556  0.7
Bonus:  0.5
C:\Users\Antek\anaconda3\lib\site-packages\sklearn\utils\validation.py:1111: DataConversionWarning:

A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().

Accuracy: Train: 0.790431558113611 Test: 0.792835209825998
Bias detected in 2 metrics: PPV, FPR

Conclusion: your model is not fair because 2 or more criteria exceeded acceptable limits set by epsilon.

Ratios of metrics, based on 'male'. Parameter 'epsilon' was set to 0.8 and therefore metrics should be within (0.8, 1.25)
             TPR       ACC       PPV       FPR   STP
female  1.083665  1.155378  0.467114  1.638889  0.82

Final plot aggregated¶

In [184]:
fobject.plot(bonus_models+[fobject2], show=False).\
    update_layout(autosize=False, width=950, height=450, legend=dict(yanchor="top", y=1.29, xanchor="right", x=0.99))